package org.example.demoschedules.service;

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.example.demoschedules.model.OrderedMap;

import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 做五休二自动排班，优先保证安排到周末上班的用户周末两天连上
 *
 * @author 单红宇
 * @since 2024-10-11 16:09:58
 */
@Setter
@Slf4j
public class ScheduleByW5R2 {

    /**
     * 配置不衔接排班第一周的连休两天分布
     * 数字代表星期几
     * 例如 {1, 2},{3, 4} 表示需要安排2个人在周末上班，则第一个人是周一周二休息，周三到周日上班，第二个人是周三周四休息周一周二周五周六周日上班；
     * 如果周末上班的人数超过了数组的大小，则自动从数组下标为0开始循环分配
     * 注意：因为要本类的代码规则优先保证被安排在周末上班的两天都要上班，所以这里设定初始休息星期几的时候，不可以包含6和7
     */
    private int[][] firstWeekRestDistribution = {{1, 2}, {3, 4}, {4, 5}};

    /**
     * 将都好分隔开的上班和休息字符串转换为排班集合
     *
     * @param startDate   startDate
     * @param workRestStr 工作休息字符串，示例：上班,上班,休息,休息,上班,上班,上班
     * @return OrderedMap
     */
    public OrderedMap<LocalDate, WorkStatus> generateUserWeekSchedule(LocalDate startDate, String workRestStr) {
        OrderedMap<LocalDate, WorkStatus> userWeekSchedule = new OrderedMap<>();
        String[] arr = workRestStr.split(",");
        for (int i = 0; i < arr.length; i++) {
            userWeekSchedule.put(startDate.plusDays(i), WorkStatus.valueOf(arr[i]));
        }
        return userWeekSchedule;
    }

    /**
     * 生成排班
     *
     * @param startDate          排班开始日期（必须是周一）
     * @param weeks              排班周数
     * @param users              排班的用户集合（如果不是衔接排班，则传入的用户顺序，会按照数组firstWeekRestDistribution变量定义的休息顺序进行排序，所以你可以根据<br/>
     *                           实际需求通过改编用户集合中的顺序来调整休息日。比如第二个月排班时，把上个月周一周二休息的用户放最后一个，他就不会被优先排在周末上班了）
     * @param weekendStaffCount  周末排班人数（周六和周日需要安排几个人上班）
     * @param noScheduleDates    完全不排班的日期集合（指定日期所有全部休息，例如全民放假等特定日期）
     * @param beforeWeekSchedule 上一个周期排班最后一周的排班
     * @param isLinking          是否衔接排班（指衔接上一个排班周期继续排班）
     * @return Map &lt;姓名, OrderedMap&lt;日期, 上班或休息&gt;&gt;
     */
    public OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> generateSchedule(
            LocalDate startDate, int weeks, List<String> users, int weekendStaffCount, Set<LocalDate> noScheduleDates,
            final OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> beforeWeekSchedule, boolean isLinking) {
        boolean linkingSchedule = isLinking;
        // 如果参数weekendStaffCount还大于用户集合，则肯定是不合法的调用
        if (weekendStaffCount > users.size()) {
            return new OrderedMap<>();
        }

        // 如果需要衔接排班，但是lastDaySchedule又为空，则按照不衔接排班处理
        if (linkingSchedule && (beforeWeekSchedule == null || beforeWeekSchedule.isEmpty())) {
            linkingSchedule = false;
        }

        // 为每个用户初始化用户排班表容器
        OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> allWeekSchedule = new OrderedMap<>();
        for (String user : users) {
            allWeekSchedule.put(user, new OrderedMap<>());
        }

        OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> weekSchedule = null;
        // 进行后面几周排班
        for (int week = 0; week < weeks; week++) {
            // 第二周开始linkingSchedule固定为true
            weekSchedule = this.generateWeekSchedule(startDate.plusWeeks(week),
                    users, weekendStaffCount, week == 0 ? beforeWeekSchedule : weekSchedule,
                    week != 0 || linkingSchedule); // 第一周week=0时使用上一个周期最后一周排班数据和使用指定是否衔接排班
            // 将排班结果装入结果容器，并提取最后一天排班
            putWeekScheduleToResult(allWeekSchedule, weekSchedule);
        }

        // 处理所有固定休息日
        this.clearNoScheduleDates(allWeekSchedule, noScheduleDates);

        return allWeekSchedule;
    }

    /**
     * 设置所有固定不排班的日期
     *
     * @param allWeekSchedule 所有排班表
     * @param noScheduleDates 不排班的日期，固定设置排班表为休息
     */
    public void clearNoScheduleDates(OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> allWeekSchedule, Set<LocalDate> noScheduleDates) {
        for (Map.Entry<String, OrderedMap<LocalDate, WorkStatus>> mapEntry : allWeekSchedule.entrySet()) {
            noScheduleDates.forEach(d -> mapEntry.getValue().put(d, WorkStatus.REST));
        }
    }

    /**
     * 将一周的排班记录存储到结果排班表中
     *
     * @param allWeekSchedule 所有排班表容器
     * @param weekSchedule    一周的排班表
     */
    private void putWeekScheduleToResult(OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> allWeekSchedule,
                                         OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> weekSchedule) {
        if (allWeekSchedule == null || weekSchedule == null) {
            return;
        }
        for (Map.Entry<String, OrderedMap<LocalDate, WorkStatus>> mapEntry : allWeekSchedule.entrySet()) {
            mapEntry.getValue().putAll(weekSchedule.get(mapEntry.getKey()));
        }
    }

    /**
     * 计算并生成下一周的排班表
     *
     * @param startDate          排班开始日期（必须是周一）
     * @param users              排班的用户集合
     * @param weekendStaffCount  周末排班人数（周六和周日需要安排几个人上班）
     * @param beforeWeekSchedule 上一个周期排班最后一周的排班
     * @param linkingSchedule    是否衔接排班（指衔接上一个排班周期继续排班）
     * @return Map&lt;姓名, OrderedMap&lt;日期, 上班或休息&gt;&gt;
     */
    @SuppressWarnings({"java:S3776", "java:S6541"})
    private OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> generateWeekSchedule(
            LocalDate startDate, List<String> users, int weekendStaffCount,
            OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> beforeWeekSchedule, boolean linkingSchedule) {
        OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> schedule = new OrderedMap<>();
        for (String user : users) {
            schedule.put(user, new OrderedMap<>());
        }

        if (linkingSchedule) {
            // 对所有用户进行续接排班
            // 1.如果是上一周存在的用户，需要判断上一周最后一天是上班还是休息。
            // 1-1.如果是上班，则需要调用方法countLastConsecutiveWorkDays获得上一周连续上班的天数；如果是休息，则调用countLastConsecutiveRestDays获得上一周连续休息的天数。
            // 2.如果最后一天是上班，且连续上班天数只有一天，则续接3天上班，再续接2天休息，再续接2天上班，累计7天。
            // 2-1.如果最后一天是上班，且连续上班天数大于1小于5，则续接5减连续上班n天数，然后紧接着2天休息日，然后再补足上班天数，累计7天。
            // 2-2.如果最后一天是上班，且连续上班天数大于等于5，则续接2天休息，然后续接5天上班，累计7天。
            // 2-3.如果最后一天是休息，不管是休息1天还是大于1天，则直接续接5天上班日，然后补2天休息日够7天。
            // 3.如果用户不存在于上提供的上一周排班集合中，则直接进行5天工作日，然后补2天休息日够7天。
            // 4.校验已经排出的本周排班结果，确保周六周日上班的人数不低于参数weekendStaffCount设定的人数。
            // 4-1.如果少于weekendStaffCount人数，则找出周一到周五上班周六周日休息的人，将这些人按照方法generateWeekScheduleNotLinking的错位规则读取变量firstWeekRestDistribution进行调整上班和休息日。

            for (String user : users) {
                OrderedMap<LocalDate, WorkStatus> lastWeekSchedule = beforeWeekSchedule.get(user);
                if (lastWeekSchedule != null) {
                    WorkStatus lastStatus = lastWeekSchedule.lastEntry().getValue();
                    if (lastStatus == WorkStatus.WORK) {
                        int consecutiveWorkDays = countLastConsecutiveWorkDays(lastWeekSchedule);
                        if (consecutiveWorkDays == 1) {
                            // 续接3天上班，再续接2天休息，再续接2天上班（即周四周五休息）
                            for (int i = 0; i < 7; i++) {
                                schedule.get(user).put(startDate.plusDays(i), (i >= 3 && i < 3 + 2) ? WorkStatus.REST : WorkStatus.WORK);
                            }
                        } else if (consecutiveWorkDays >= 2 && consecutiveWorkDays <= 4) {
                            // 续接5减连续上班n天数，然后紧接着2天休息日，然后再补足上班天数
                            int remainingWorkDays = 5 - consecutiveWorkDays;
                            for (int i = 0; i < 7; i++) {
                                schedule.get(user).put(startDate.plusDays(i),
                                        (i >= remainingWorkDays && i < remainingWorkDays + 2) ? WorkStatus.REST : WorkStatus.WORK);
                            }
                        } else if (consecutiveWorkDays >= 5) {
                            // 续接2天休息，然后续接5天上班
                            for (int i = 0; i < 7; i++) {
                                schedule.get(user).put(startDate.plusDays(i), i < 2 ? WorkStatus.REST : WorkStatus.WORK);
                            }
                        }
                    } else {
                        int consecutiveRestDays = this.countLastConsecutiveRestDays(lastWeekSchedule);
                        if (consecutiveRestDays >= 1) {
                            // 如果最后一天是休息，不管是休息1天还是大于1天，则直接续接5天上班日，然后补2天休息日够7天
                            for (int i = 0; i < 7; i++) {
                                schedule.get(user).put(startDate.plusDays(i), i < 5 ? WorkStatus.WORK : WorkStatus.REST);
                            }
                        }
                    }
                } else {
                    // 如果用户不存在于上提供的上一周排班集合中，则直接进行5天工作日，然后补2天休息日够7天
                    for (int i = 0; i < 7; i++) {
                        schedule.get(user).put(startDate.plusDays(i), i < 5 ? WorkStatus.WORK : WorkStatus.REST);
                    }
                }
            }

            // 校验已经排出的本周排班结果，确保周六周日上班的人数不低于参数weekendStaffCount设定的人数
            int currentWeekendStaffCount = 0;
            for (String user : users) {
                if (schedule.get(user).get(startDate.plusDays(5)) == WorkStatus.WORK &&
                        schedule.get(user).get(startDate.plusDays(6)) == WorkStatus.WORK) {
                    currentWeekendStaffCount++;
                }
            }

            if (currentWeekendStaffCount < weekendStaffCount) {
                // 找出周一到周五上班周六周日休息的人，将这些人按照方法generateWeekScheduleNotLinking的错位规则读取变量firstWeekRestDistribution进行调整上班和休息日
                List<String> usersToAdjust = new ArrayList<>();
                for (String user : users) {
                    if (schedule.get(user).get(startDate.plusDays(5)) == WorkStatus.REST &&
                            schedule.get(user).get(startDate.plusDays(6)) == WorkStatus.REST) {
                        usersToAdjust.add(user);
                    }
                }

                int adjustedCount = 0;
                for (String user : usersToAdjust) {
                    if (adjustedCount >= weekendStaffCount - currentWeekendStaffCount) {
                        break;
                    }
                    int[] restDays = firstWeekRestDistribution[adjustedCount % firstWeekRestDistribution.length];
                    this.exchangeRestDays(schedule.get(user), startDate, restDays);
                    adjustedCount++;
                }
            }
        } else {
            // 单独排班
            return this.generateWeekScheduleNotLinking(startDate, users, weekendStaffCount);
        }

        return schedule;
    }

    /**
     * 设定数字指定日期为休息日，然后本周其他日期设置为上班
     *
     * @param userSchedule          userSchedule
     * @param mondayDate            星期一的日期
     * @param exchangeAfterRestDays 代表星期几的数字数组，例如 {1,2} 表示星期一和星期二
     */
    private void exchangeRestDays(OrderedMap<LocalDate, WorkStatus> userSchedule, LocalDate mondayDate, int[] exchangeAfterRestDays) {
        for (int i = 0; i < 7; i++) {
            userSchedule.put(mondayDate.plusDays(i), WorkStatus.WORK);
        }
        for (int restDay : exchangeAfterRestDays) {
            LocalDate restDate = mondayDate.plusDays(restDay - 1L);
            userSchedule.put(restDate, WorkStatus.REST);
        }
    }

    /**
     * 不衔接排班
     *
     * @param startDate         开始日期
     * @param users             用户集合
     * @param weekendStaffCount 周末安排几个人上班
     * @return 排班结果集合
     */
    private OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> generateWeekScheduleNotLinking(LocalDate startDate, List<String> users,
                                                                                                 int weekendStaffCount) {
        OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> schedule = new OrderedMap<>();
        for (String user : users) {
            schedule.put(user, new OrderedMap<>());
        }

        LocalDate currentDate = startDate;

        // 第一步：将所有用户周一到周五设置为上班，周六和周日设置为休息
        for (int day = 0; day < 7; day++, currentDate = currentDate.plusDays(1)) {
            for (String user : users) {
                if (day < 5) {
                    schedule.get(user).put(currentDate, WorkStatus.WORK);
                } else {
                    schedule.get(user).put(currentDate, WorkStatus.REST);
                }
            }
        }

        // 第二步：随机取出对应数量的用户，将他们按照数组的顺序设置对应工作日为休息，并且设置周末2天为上班
        List<String> shuffledUsers = new ArrayList<>(users);
        Collections.shuffle(shuffledUsers);

        for (int i = 0; i < weekendStaffCount; i++) {
            String user = shuffledUsers.get(i);
            int[] restDays = firstWeekRestDistribution[i % firstWeekRestDistribution.length];
            // 设置对应工作日为休息，周末2天为上班
            this.exchangeRestDays(schedule.get(user), startDate, restDays);
        }

        return schedule;
    }

    /**
     * 目标排班集合中，末尾连续工作天数
     *
     * @param userSchedule userSchedule
     * @return data
     */
    public int countLastConsecutiveWorkDays(OrderedMap<LocalDate, WorkStatus> userSchedule) {
        return this.countLastConsecutiveWorkStatusDays(userSchedule, WorkStatus.WORK, null);
    }

    /**
     * 目标排班集合中，末尾连续休息天数
     *
     * @param userSchedule userSchedule
     * @return data
     */
    public int countLastConsecutiveRestDays(OrderedMap<LocalDate, WorkStatus> userSchedule) {
        return this.countLastConsecutiveWorkStatusDays(userSchedule, WorkStatus.REST, null);
    }

    /**
     * 目标排班集合中，指定日期(包含)之前连续连续工作或休息天数，按参数指定工作状态计算
     *
     * @param userSchedule userSchedule
     * @param workStatus   workStatus
     * @param date         date
     * @return data
     */
    public int countLastConsecutiveWorkStatusDays(OrderedMap<LocalDate, WorkStatus> userSchedule, WorkStatus workStatus, LocalDate date) {
        int consecutiveWorkDays = 0;
        LocalDate targetDate = date == null ? userSchedule.lastKey() : userSchedule.nextKey(date);
        while (targetDate != null && workStatus.equals(userSchedule.get(targetDate))) {
            consecutiveWorkDays++;
            targetDate = userSchedule.previousKey(targetDate);
        }
        return consecutiveWorkDays;
    }

    /**
     * 将排班表输出到 HTML 文件
     *
     * @param schedule 排班表
     */
    public void printScheduleToHTML(OrderedMap<String, OrderedMap<LocalDate, WorkStatus>> schedule) {
        // 获取所有日期
        DateTimeFormatter weekFormatter = DateTimeFormatter.ofPattern("EEEE", Locale.SIMPLIFIED_CHINESE);
        List<LocalDate> allLocalDates = new ArrayList<>(schedule.values().stream().findFirst().orElseGet(OrderedMap::new).keySet());
        List<String> allDates = allLocalDates.stream().map(LocalDate::toString).toList();
        List<String> allWeeks = allLocalDates.stream().map(date -> date.format(weekFormatter)).toList();

        // 因为 freemarker 中无法使用 schedule[user][LocalDate]这样取值，所以把LocalDate转换为String日期格式
        // 即：将 Map<String, OrderedMap<LocalDate, WorkStatus>> schedule 转换为 Map<String, OrderedMap<String, WorkStatus>> schedule
        Map<String, Map<String, WorkStatus>> allSchedule = schedule.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,
                entry -> entry.getValue().entrySet().stream().collect(Collectors.toMap(e -> e.getKey().toString(), Map.Entry::getValue))));

        // 准备数据模型
        Map<String, Object> model = new HashMap<>();
        model.put("dates", allDates);
        model.put("weeks", allWeeks);
        model.put("allSchedule", allSchedule);

        // 配置 FreeMarker
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
        try {
            // 设置模板加载路径为 resources 目录
            cfg.setClassForTemplateLoading(ScheduleByW5R2.class, "/templates");
            Template template = cfg.getTemplate("schedule.ftl");

            // 生成文件名
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
            String fileName = "schedule-" + LocalDateTime.now().format(formatter) + ".html";

            // 获取系统临时目录
            Path tempDir = Paths.get(System.getProperty("java.io.tmpdir"));
            Path outputPath = tempDir.resolve(fileName);

            // 写入 HTML 文件
            try (FileWriter writer = new FileWriter(outputPath.toFile())) {
                template.process(model, writer);
                log.info("排班表已成功生成到文件: file:///" + outputPath.toAbsolutePath().toString().replace("\\", "/"));
            }
        } catch (IOException | TemplateException e) {
            log.error("生成排班表时发生错误: ", e);
        }
    }

}